# coding:utf-8
import datetime
import csv
import math
import os
import json
import time
from collections import defaultdict
from ztp_backend.settings import IMEI_BLACK_LIST, WITH_WX
from django.core.management.base import BaseCommand
from django.utils import timezone
from django_redis import get_redis_connection
from mongolog.models import AppsLog, CASLog, StrategyLog
from mongolog.utils import date_eq, date_range
from zmad.models import ZTPUser, ZTPResource
from strategy.models import Strategy, SafetyCreditEvaluation, CreditGlobalAction
from userdata.training.anomaly import SafetyScoreDetection
from userdata.training import adapters
from userdata.models import UserSafetyScore, UserScore, UserAddScore, UserCreditScore, UserCreditScoreDetails
from tools.datetime import as_datetime, to_date

from strategy.migrations import (
    CATEGORY_AUTH, CATEGORY_TERMINAL, CATEGORY_RESOURCE,
    CATEGORY_ENV, CATEGORY_DEVICE
)

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

device_factor = 1.2
resources_factor = 1.3
behavior_factor = 1.1
env_factor = 1.1
terminal_factor = 1.2
bonus_points = 3
behavioral_threshold = 0.9
behavior_preference_score = 10
bonus_point_coefficient = [0.2, 0.15, 0.1, 0.1, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05]  # 加分项的系数
max_strategy_points = 30  # 同一类别策略当天扣分上限, 加分上限


class Command(BaseCommand):
    help = "Update user's safety reputation score. "

    def add_arguments(self, parser):
        pass

    def handle(self, *args, **options):
        # 拿到所有的用户
        if WITH_WX:
            all_user = ZTPUser.objects.filter(enabled=True).exclude(wx_user_id="").all()
        else:
            all_user = ZTPUser.objects.filter(enabled=True).all()
        # print(" *************** update_user_score *************** ", to_date(datetime.datetime.now()))
        # 获取应用资源的id列表，用于构建用户访问资源页面向量
        resource_id_list = list(ZTPResource.objects.values_list('id', flat=True))
        # 获取策略id和对应的分值
        uid_score_dict = get_uid_score_dict()
        # 用户当日信誉分等级
        ticks = CreditGlobalAction.objects.distinct().values_list('score_lower', flat=True).order_by('score_lower')
        for user in all_user:
            now_date = datetime.datetime.now()
            # 用户信誉分更新的最新日期，年月日格式.用于控制同一天内信誉分多次计算
            if UserCreditScoreDetails.objects.filter(user=user).first():
                latest_date = UserCreditScoreDetails.objects.filter(
                    user=user).values_list('datetime', flat=True).order_by('-datetime')[0]
            else:
                latest_date = ""  # 没有历史信誉分的用户，设置最近一次信誉分时间记录为 "".

            # 解决时区不一致问题.启动信誉分计算的条件：{
            # 1，用户无上一次信誉分计算记录
            # 2，现在日期的时间是 凌晨2点到4点之间，并且上一次信誉分记录时间是昨天。
            # }
            # 需要 1 or 2，即可。
            now_hour = now_date.hour  # 获取当前时间的小时
            if (latest_date == "") or (to_date(latest_date) != to_date(now_date) and 2 <= now_hour <= 3
                                       and now_date.timestamp() - latest_date.timestamp() >= 7200):
                print("*" * 60, user.username)
                print("***** start update_user_score *****")
                print("latest_date : ", latest_date)
                print("now_date : ", now_date)
                user_name = user.username
                terminal_threat, access_resources, env_baseline, behavior_preference, access_devices = 0, 0, 0, 0, 0
                bonus_point = 0,
                try:
                    # 示例：strategy_dict = {'access_devices': [], 'terminal_threat': [],
                    # 'behavior_preference': [{'STG/AUTH-2': 3}],'env_baseline': [], 'access_resources': []}
                    strategy_dict = self.get_strategy(user_name, uid_score_dict, days_num=1)
                    terminal_threat, access_resources, env_baseline, behavior_preference, access_devices = \
                        get_strategy_score(strategy_dict)
                    ip_score, time_score, device_score, login_score, cas_score = self.get_behaviour_score(user_name, 1)
                    behavior_preference = behavior_preference + login_score + time_score
                    access_devices = access_devices + device_score + ip_score
                    access_resources = access_resources + cas_score
                    # 计算用户行为分，基于用户在不同时间窗口内访问资源的相似性来计算
                    similarity = get_behavior_vector(user_name, resource_id_list)
                    if similarity < behavioral_threshold:
                        # 计算用户行为异常扣分，向上取整
                        behavior_preference += math.ceil(behavior_preference_score * (1 - similarity))
                    # 获取用户加分信息
                    bonus_point = min(self.count_user_bonus_point(user_name, days_num=15), max_strategy_points)  # 用户加分
                except:
                    pass

                # 用户最新信誉分表同步
                danger_nums = json.dumps({"danger_nums": 0})
                user.score_detail = danger_nums
                us = UserCreditScore.objects.get_or_create(user=user)[0]
                # 修复每天扣分项的上限
                us.terminal_score = max(us.terminal_score - min(terminal_threat, max_strategy_points), 0)
                us.resources_score = max(us.resources_score - min(access_resources, max_strategy_points), 0)
                us.env_score = max(us.env_score - min(env_baseline, max_strategy_points), 0)
                us.behavior_score = max(us.behavior_score - min(behavior_preference, max_strategy_points), 0)
                us.devices_score = max(us.devices_score - min(access_devices, max_strategy_points), 0)
                us.credit_score = us.terminal_score + us.resources_score + us.env_score + us.behavior_score + \
                                  us.devices_score + bonus_point
                us.score_detail = danger_nums
                us.save()
                ZTPUser.objects.filter(username=user.username).update(safety_score=us.credit_score)

                # 更新用户信誉分的详情表
                # 基于用户当日信誉分，计算用户信誉分风险等级
                level = 0
                for i in range(len(ticks)):
                    if us.credit_score < ticks[i]:
                        level = 4 - i
                        break
                user_credit_score = UserCreditScoreDetails(terminal_threat=terminal_threat,
                                                           access_resources=access_resources,
                                                           env_baseline=env_baseline,
                                                           behavior_preference=behavior_preference,
                                                           access_devices=access_devices,
                                                           bonus_point=bonus_point,
                                                           credit_score=us.credit_score,
                                                           level=level,
                                                           user=user)
                user_credit_score.save()
                deduplication(user)
                time.sleep(0.1)
            else:
                pass

    def get_strategy(self, user_name, uid_score_dict, days_num=1):
        """ 从 StrategyLog 表里面获取一定周期天数里用户触发的策略"""
        interval_days = datetime.timedelta(days=days_num)
        date_to = datetime.datetime.now()
        date_from = as_datetime(date_to - interval_days)
        # 触发策略数据
        strategy_query = StrategyLog.objects.mongo_aggregate([
            {
                '$match': {
                    'username': user_name,
                    'log_time': {"$gt": date_from, "$lt": date_to},
                },

            },
            {
                '$project': {
                    'uid': 1,
                    'source': 1,
                    'username': 1,
                    'date': 1,
                    'platform': 1,
                    'meta': 1,
                }
            },
        ])
        device_list, resources_list, behavior_list, env_list, terminal_list = [], [], [], [], []
        for strategy_query_i in strategy_query:
            uid = strategy_query_i["uid"]
            try:
                uid_score_ = {uid: uid_score_dict[uid]}
                if uid.startswith(CATEGORY_TERMINAL[1]):
                    terminal_list.append(uid_score_)
                elif uid.startswith(CATEGORY_AUTH[1]):
                    behavior_list.append(uid_score_)
                elif uid.startswith(CATEGORY_ENV[1]):
                    env_list.append(uid_score_)
                elif uid.startswith(CATEGORY_RESOURCE[1]):
                    resources_list.append(uid_score_)
                elif uid.startswith(CATEGORY_DEVICE[1]):
                    device_list.append(uid_score_)
            except:
                pass

        strategy_dict = {"access_devices": device_list,
                         "terminal_threat": terminal_list,
                         "behavior_preference": behavior_list,
                         "env_baseline": env_list,
                         "access_resources": resources_list,
                         }

        # print("strategy_dict : ", strategy_dict)
        return strategy_dict

    def get_behaviour_score(self, user_name, days_num=1):
        """ 基于用户数据，获取用户多维度的（非策略部分）扣分项
        """
        interval_days = datetime.timedelta(days=days_num)
        date_to = datetime.datetime.now()
        date_from = as_datetime(date_to - interval_days)
        cas_query = CASLog.objects.filter(username=user_name, log_time__gt=date_from, log_time__lt=date_to)

        # 登录异常 一个user有多次登录记录会增加登录分
        login_score = 0
        try:
            cas_login_query = cas_query.filter(auth_code__in=[1, 2]).count()
            if cas_login_query <= 2:
                login_score += 0
            elif cas_login_query <= 5:
                login_score += 1
            elif cas_login_query > 5:
                login_score += 2
        except:
            pass
        login_score = min(login_score, 3)

        # 设备异常  一个user对应多台设备uuid会增加设备分
        cas_query_mongo = CASLog.objects.mongo_aggregate([
            {
                '$match': {
                    'username': user_name,
                    'log_time': {"$gt": date_from, "$lt": date_to},
                }
            },
            {
                '$project': {
                    'username': 1,
                    'client_ip': 1,
                    'device': 1,
                    'device_uuid': 1,
                    'log_time': 1,
                    'url': 1,
                    'resource': 1,
                }
            },
        ])
        device_score = 0
        device_list = []
        for cas_query_i in cas_query_mongo:
            try:
                if cas_query_i["device"] not in device_list:
                    device_list.append(cas_query_i["device"])
            except:
                pass
        device_nums = len(device_list)
        if 1 < device_nums <= 2:
            device_score += 1
        elif 3 <= device_nums <= 4:
            device_score += 3
        elif device_nums >= 5:
            device_score += 5
        device_score = min(device_score, 7)

        # 时间异常
        time_score = 0
        for cas_query_i in cas_query_mongo:
            format_time = datetime_format(cas_query_i["log_time"])
            hour = format_time[0]
            minute = format_time[1]
            if 21 < hour < 23 or (hour == 23 and minute == 0):
                time_score += 1
            elif hour == 23 and minute != 0 or (hour > 23 or hour < 1) or (hour == 1 and minute == 0):
                time_score += 3
            elif (hour == 1 and minute != 0) or 1 < hour < 9 or (hour == 9 and minute == 0):
                time_score += 5
        time_score = min(time_score, 7)

        # 访问异常
        cas_score = 0
        try:
            redis = get_redis_connection('anomaly_detection')
            adapter = adapters.RedisAdapter(redis)
            ad_model = SafetyScoreDetection(max_data=100, min_data=1, outlier=500, adapter=adapter)
            # 重新生成训练数据
            ad_model.clear()
            days = 15
            train_data = self.count_user_activity(days)
            ad_model.train(train_data)
            detection_nums = cas_query.count()
            for _ in range(detection_nums):
                cas_score = ad_model.detection(user_name)
        except:
            pass
        cas_score = min(cas_score, 7)

        # 地点异常
        cas_location_query = CASLog.objects.mongo_aggregate([
            {
                '$match': {
                    'username': user_name,
                    'log_time': {"$gt": date_from, "$lt": date_to}
                },
            },
            {
                '$group': {
                    '_id': 'ip_location.city_name'
                }
            }
        ])
        location_nums = 0
        ip_score = 0
        for clquery in cas_location_query:
            location_nums += 1
        if location_nums == 1:
            pass
        elif location_nums == 2:
            ip_score += 3
        elif location_nums >= 3:
            ip_score += 5
        ip_score = min(ip_score, 7)

        return ip_score, time_score, device_score, login_score, cas_score

    def count_user_bonus_point(self, user, days_num=15):
        """计算用户加分项"""
        bonus_point = 0
        try:
            interval_days = datetime.timedelta(days=days_num)
            date_to = datetime.datetime.now()
            date_from = as_datetime(date_to - interval_days)

            bonus_point_query = UserCreditScoreDetails.objects.filter(user=user, datetime__gt=date_from,
                                                                      datetime__lt=date_to).order_by('-datetime')
            deduct_points_list = list()
            for query_i in bonus_point_query:
                deduct_points = query_i.terminal_threat + query_i.access_resources + query_i.env_baseline + \
                                query_i.behavior_preference + query_i.access_devices
                deduct_points_list.append(deduct_points)
            for i in range(len(deduct_points_list)):
                bonus_point += deduct_points_list[i] * bonus_point_coefficient[i]
        except:
            pass

        return math.ceil(bonus_point)


# 用于格式化datetime对象,可以用来判断当前的时间属于哪个区间
def datetime_format(auth_time):
    """
    :param auth_time: Datetime对象
    :return: 一个tuple，第一个元素表示时间，第二个元素表示具体分钟
    """
    hour_min = auth_time.strftime("%H-%M")
    return tuple(map(int, hour_min.split("-")))


def get_uid_score_dict():
    """
    获取策略对应分值
    """
    uid_score_dict = {}
    strategy_query = SafetyCreditEvaluation.objects.all()
    for strategy_query_i in strategy_query:
        for uid in strategy_query_i.uid:
            uid_score_dict[uid] = strategy_query_i.score

    return uid_score_dict


def get_strategy_score(strategy_dict):
    """ 计算触发策略的扣分项
        terminal_threat         # 终端威胁扣分
        access_resources        # 访问资源扣分
        env_baseline            # 环境基线扣分
        behavior_preference     # 行为偏好扣分
        access_devices          # 访问设备扣分
    """
    terminal_threat, access_resources, env_baseline, behavior_preference, access_devices = 0, 0, 0, 0, 0
    if strategy_dict["terminal_threat"]:
        list_ = [list(d.values())[0] for d in strategy_dict["terminal_threat"]]
        terminal_threat = sum(list_) * math.pow(terminal_factor, len(list_) - 1)
    if strategy_dict["access_resources"]:
        list_ = [list(d.values())[0] for d in strategy_dict["access_resources"]]
        access_resources = sum(list_) * math.pow(resources_factor, len(list_) - 1)
    if strategy_dict["env_baseline"]:
        list_ = [list(d.values())[0] for d in strategy_dict["env_baseline"]]
        env_baseline = sum(list_) * math.pow(env_factor, len(list_) - 1)
    if strategy_dict["behavior_preference"]:
        list_ = [list(d.values())[0] for d in strategy_dict["behavior_preference"]]
        behavior_preference = sum(list_) * math.pow(behavior_factor, len(list_) - 1)
    if strategy_dict["access_devices"]:
        list_ = [list(d.values())[0] for d in strategy_dict["access_devices"]]
        access_devices = sum(list_) * math.pow(device_factor, len(list_) - 1)

    # terminal_threat = min(math.ceil(terminal_threat), max_strategy_points)
    # access_resources = min(math.ceil(access_resources), max_strategy_points)
    # env_baseline = min(math.ceil(env_baseline), max_strategy_points)
    # behavior_preference = min(math.ceil(behavior_preference), max_strategy_points)
    # access_devices = min(math.ceil(access_devices), max_strategy_points)

    return int(terminal_threat), int(access_resources), int(env_baseline), int(behavior_preference), int(access_devices)


def get_behavior_vector(user_name, resource_id_list):
    """ 计算用户在不同时间窗口访问资源的行为向量相似度
    """
    today = datetime.date.today()
    tz_name = timezone.get_default_timezone_name()
    queryset_21 = CASLog.objects.mongo_aggregate([
        {
            '$match': {
                'username': user_name,  # user_name
                'resource': {
                    '$exists': True,
                    '$ne': ''
                },
                **date_range('log_time', date_from=today - datetime.timedelta(days=29),
                             date_to=today + datetime.timedelta(days=7))
            }
        },
        {
            '$group': {
                '_id': {
                    'year': {'$year': {'date': '$log_time', 'timezone': tz_name}},
                    'month': {'$month': {'date': '$log_time', 'timezone': tz_name}},
                    'day': {'$dayOfMonth': {'date': '$log_time', 'timezone': tz_name}},
                    'resource': '$resource',
                },
                'count': {'$sum': 1}
            }
        },
        {
            '$project': {
                '_id': 0,
                'resource': '$_id.resource',
                'date': ['$_id.year', '$_id.month', '$_id.day'],
                'count': 1
            }
        }
    ])
    queryset_7 = CASLog.objects.mongo_aggregate([
        {
            '$match': {
                'username': user_name,
                'resource': {
                    '$exists': True,
                    '$ne': ''
                },
                **date_range('log_time', date_from=today - datetime.timedelta(days=7),
                             date_to=today + datetime.timedelta(days=0))
            }
        },
        {
            '$group': {
                '_id': {
                    'year': {'$year': {'date': '$log_time', 'timezone': tz_name}},
                    'month': {'$month': {'date': '$log_time', 'timezone': tz_name}},
                    'day': {'$dayOfMonth': {'date': '$log_time', 'timezone': tz_name}},
                    'resource': '$resource',
                },
                'count': {'$sum': 1}
            }
        },
        {
            '$project': {
                '_id': 0,
                'resource': '$_id.resource',
                'date': ['$_id.year', '$_id.month', '$_id.day'],
                'count': 1
            }
        }
    ])
    # 组装数据
    resource2count_21, resource2count_7 = defaultdict(int), defaultdict(int)
    for agg in queryset_21:
        resource2count_21[agg['resource']] += agg['count']

    for agg in queryset_7:
        resource2count_7[agg['resource']] += agg['count']
    resource_vec_21, resource_vec_7 = [1] * len(resource_id_list), [1] * len(resource_id_list)
    for resource_id in resource_id_list:
        index_ = resource_id_list.index(resource_id)
        if resource2count_21[resource_id]:
            resource_vec_21[index_] = resource2count_21[resource_id]
        if resource2count_7[resource_id]:
            resource_vec_7[index_] = resource2count_7[resource_id]
    # print("resource_vec_7, resource_vec_21 : ", resource_vec_7, resource_vec_21)
    similarity = cosine_similarity(resource_vec_7, resource_vec_21)

    return similarity


def cosine_similarity(l_1, l_2):
    """ 计算两个向量的夹角余弦相似度
    """
    dot_product = sum([l_1[i] * l_2[i] for i in range(len(l_1))])
    norm1 = math.sqrt(sum([x ** 2 for x in l_1]))
    norm2 = math.sqrt(sum([x ** 2 for x in l_2]))
    similarity = dot_product / (norm1 * norm2)

    return similarity


def deduplication(user):
    latest_date_list = UserCreditScoreDetails.objects.filter(user=user
        ).values_list('datetime', flat=True).order_by('-datetime')
    # 进行去重，一天只保留1条数据
    date_list = []
    latest_date_list_new = []
    for latest_date_ in latest_date_list:
        if to_date(latest_date_) not in date_list:
            date_list.append(to_date(latest_date_))
            latest_date_list_new.append(latest_date_)

    for latest_date_ in latest_date_list:
        if latest_date_ not in latest_date_list_new:
            UserCreditScoreDetails.objects.filter(user=user, datetime=latest_date_).delete()
